package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

public class Server_v3 {
    /*
    *java.net.ServerSocket
    * 运行在服务器的ServerSocket主要有两个工作：
    * 1.打开服务器端口，客户端就是通过这个端口与服务端建立链接的
    * 2.监听服务器端口，一旦一个客户端连接，则立即返回一个Socket实例 */
    private ServerSocket serverSocket;
    //存放所有客户端的输出流，用于广播消息
    private List<PrintWriter> allOut = new ArrayList<>();

    public Server_v3(){
        try {
            /*
            * ServerSocket实例化的同时指定服务器端口
            * 如果该端口被其他程序占用则会抛出异常：
            * java.net.BindException:address already in use
            * 此时我们需要更换端口，或者杀死占用该程序的进程
            * 端口号范围：0-65535*/
            System.out.println("正在启动服务端...");
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /*
     * 服务端开始工作的方法
     * */
    public void start(){
        try {
            /*
            * serverSocket一个重要方法：
            * Socket accept()
            * 该方法用于接受客户端的连接。这是一个阻塞方法，调用后台"卡住"，
            * 直到一个客户端与serverSocket连接，此时该方法会立即返回一个Socket实例
            * 通过这个Socket实例与该客户端对等连接并进行通讯
            * 相当于"接电话"的动作*/
            while (true){
                System.out.println("等待客户端连接...");
                Socket socket = serverSocket.accept();
                System.out.println("一个客户端连接了！");
                ClientHandler handler = new ClientHandler(socket);
                Thread t = new Thread(handler);
                t.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Server_v3 server = new Server_v3();
        server.start();
    }
    /*
    * 第二种创建线程的方式：实现Runnable接口单独定义线程任务
    * 这个线程任务就是让一个线程与指定的客户端进行交互*/
    private class ClientHandler implements Runnable{
        private Socket socket;
        private String host; //客户端的地址信息
        public ClientHandler (Socket socket){
            this.socket = socket;
            host = socket.getInetAddress().getHostAddress();
        }
        public void run (){
            PrintWriter pw = null;
            try {
                /*
                 * Socket一个重要方法
                 * InputStream getInputStream
                 * 通过Socket获取的字节输入流可以读取远端计算机发送过来的数据*/
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw,true);
                //将对应该客户的输出流存入共享集合allout中
                synchronized (allOut){
                    allOut.add(pw);
                }
                //通知所有客户端该用户上线了
                sendMessage(host+"上线了，当前在线人数："+allOut.size());
                String line;
                /*
                 * java.net.SocketException: Connection reset
                 * 当客户端断开连接时，如果是异常断开，服务器这里readline方法会抛出异常*/
                while ((line = br.readLine())!=null){
                    sendMessage(host+"说："+line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                //处理该客户端下线后的操作
                //将该客户端的输出流从该共享集合中删除
                synchronized (allOut) {
                    allOut.remove(pw);
                }
                //通知所有客户端该用户下线了
                sendMessage(host+"下线了，当前在线人数："+allOut.size());
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        //将消息发送给所有客户端
        private void sendMessage(String message){
            System.out.println(message);
            //为了和增删形成互斥 因为是新循环 迭代器工作时不允许增删操作
            synchronized (allOut) {
                for (PrintWriter p : allOut) {
                    p.println(message);
                }
            }
        }
    }
}
